fix(Channel): guard render-phase channel.getConfig() against disconnected channels - #3257
Conversation
…cted channels ChannelInner evaluated channel.getConfig() as an eager useState argument, which throws "You can't use a channel after client.disconnect() was called" once the channel is disconnected (current user removed / channel deleted). The throw happened during render, tearing down the surrounding subtree. Add an internal getChannelConfig() guard, apply it at every render-phase call site, use a lazy initializer so it no longer re-runs on every render, and early-return from handleEvent for a disconnected channel. Fixing Channel alone is not enough: the crash relocates to AttachmentSelector once ChannelInner stops throwing and its subtree renders. Closes #3254
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR adds a shared disconnected-channel configuration guard. Channel rendering, events, and pagination avoid unsafe access. Composer and read-state flows skip operations that require an active client. Regression tests cover these paths. ChangesDisconnected channel safety
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Size Change: +235 B (+0.03%) Total Size: 885 kB 📦 View Changed
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/MessageComposer/MessageComposer.tsx`:
- Around line 98-105: Recheck messageComposer.channel.disconnected inside the
finally callback before calling messageComposer.clear(), while preserving the
existing early return before createDraft(). Add a test covering an unresolved
createDraft(), unmounting and disconnecting the channel, then resolving the
draft and verifying clear() is not called.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8dfb987f-5afb-479d-a989-5f3fb44ef7e7
📒 Files selected for processing (12)
src/components/Channel/Channel.tsxsrc/components/Channel/__tests__/Channel.test.tsxsrc/components/MessageComposer/AttachmentSelector/AttachmentSelector.tsxsrc/components/MessageComposer/MessageComposer.tsxsrc/components/MessageComposer/__tests__/AttachmentSelector.test.tsxsrc/components/MessageComposer/__tests__/MessageInput.test.tsxsrc/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsxsrc/components/MessageComposer/hooks/useMessageComposerCommands.tssrc/components/MessageList/hooks/__tests__/useMarkRead.test.tsxsrc/components/MessageList/hooks/useMarkRead.tssrc/utils/__tests__/getChannelConfig.test.tssrc/utils/getChannelConfig.ts
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3257 +/- ##
==========================================
+ Coverage 85.21% 85.26% +0.05%
==========================================
Files 507 509 +2
Lines 15957 15972 +15
Branches 5029 5034 +5
==========================================
+ Hits 13597 13618 +21
+ Misses 2360 2354 -6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
loadMore already short-circuits on channel.disconnected, but loadMoreNewer checked only online.current / navigator.onLine / hasNext and went on to call channel.query(), which throws for a disconnected channel. The existing try/catch swallowed it into a console.warn plus a spurious setLoadingMoreNewer dispatch on every scroll-to-bottom.
Reduce the getChannelConfig JSDoc and the inline comments to the core statement: what throws, when, and what is returned instead.
Experiment only, to be reverted. Keeps the loadMoreNewer production guard and removes only the test added alongside it, to determine whether the VirtualizedMessageList snapshot failure on CI comes from the guard itself or from the scheduling shift caused by one extra test.
Fold the loadMoreNewer assertion into the existing disconnected-pagination test instead of adding a separate one. The extra test entry shifted parallel test scheduling enough to tip a pre-existing race in the VirtualizedMessageList empty-list snapshot on CI, where react-virtuoso reported "not at bottom" and rendered the jump-to-latest button.
🎯 Goal
Fixes: #3254
ChannelInnercalledchannel.getConfig()directly in the component body. That call throwsYou can't use a channel after client.disconnect() was calledonce the channel is disconnected — which happens when the current user is removed from a channel or the channel is deleted. The flag is flipped by an async WS event while<Channel>is still mounted, so the throw landed in the render phase and tore down the surrounding subtree.Same failure class as #2393 and #3248.
🛠 Implementation details
Added an internal
getChannelConfig(channel)helper that returnsundefinedfor a disconnected channel instead of callinggetConfig(), and applied it everywhere the config was read during render or in an effect:Channel.tsx— the reported crash. Now also a lazyuseStateinitializer, so the call no longer re-runs on every render.AttachmentSelector.tsxanduseMessageComposerCommands.tsuseMarkRead.tshandleEventinChannel.tsxalso early-returns for a disconnected channel, and the composer skips draft creation on unmount.loadMoreNewerpicked up thechannel.disconnectedguard thatloadMorealready had — without it, scrolling to the bottom of a disconnected channel still queried a dead channel on every attempt (caught by the existingtry/catch, so only log noise and a redundant dispatch).Fixing
Channelalone is not enough: the crash relocates toAttachmentSelectoronceChannelInnerstops throwing and its subtree starts rendering.undefinedis already part ofgetConfig()'s return type, so degradation is graceful — no read events, no commands, and the attachment selector renders nothing instead of crashing.9 tests added, each verified to fail against the unfixed code first.
🎨 UI Changes
None.
Summary by CodeRabbit
Bug Fixes
Tests